home
***
CD-ROM
|
disk
|
FTP
|
other
***
search
/
Language/OS - Multiplatform Resource Library
/
LANGUAGE OS.iso
/
pcl
/
src-16f.lha
/
code
/
parse-time.lisp
< prev
next >
Wrap
Lisp/Scheme
|
1992-05-30
|
24KB
|
608 lines
;;; -*- Mode: Lisp; Package: Extensions; Log: code.log -*-
;;; **********************************************************************
;;; This code was written as part of the CMU Common Lisp project at
;;; Carnegie Mellon University, and has been placed in the public domain.
;;; If you want to use this code or any part of CMU Common Lisp, please contact
;;; Scott Fahlman or slisp-group@cs.cmu.edu.
;;;
(ext:file-comment
"$Header: parse-time.lisp,v 1.3 91/07/26 11:35:16 chiles Exp $")
;;;
;;; **********************************************************************
;;; Parsing routines for time and date strings. Parse-time returns the
;;; universal time integer for the time and/or date given in the string.
;;; Written by Jim Healy, June 1987.
;;; **********************************************************************
(in-package "EXTENSIONS" :use "LISP")
(export 'parse-time)
(defconstant whitespace-chars '(#\space #\tab #\newline #\, #\' #\`))
(defconstant time-dividers '(#\: #\.))
(defconstant date-dividers '(#\\ #\/ #\-))
(defvar *error-on-mismatch* nil
"If t, an error will be signalled if parse-time is unable
to determine the time/date format of the string.")
;;; Set up hash tables for month, weekday, zone, and special strings.
;;; Provides quick, easy access to associated information for these items.
;;; Hashlist takes an association list and hashes each pair into the
;;; specified tables using the car of the pair as the key and the cdr as
;;; the data object.
(defmacro hashlist (list table)
`(dolist (item ,list)
(setf (gethash (car item) ,table) (cdr item))))
(defparameter weekday-table-size 23)
(defparameter month-table-size 31)
(defparameter zone-table-size 11)
(defparameter special-table-size 11)
(defvar *weekday-strings* (make-hash-table :test #'equal
:size weekday-table-size))
(defvar *month-strings* (make-hash-table :test #'equal
:size month-table-size))
(defvar *zone-strings* (make-hash-table :test #'equal
:size zone-table-size))
(defvar *special-strings* (make-hash-table :test #'equal
:size special-table-size))
;;; Load-time creation of the hash tables.
(hashlist '(("monday" . 0) ("mon" . 0)
("tuesday" . 1) ("tues" . 1) ("tue" . 1)
("wednesday" . 2) ("wednes" . 2) ("wed" . 2)
("thursday" . 3) ("thurs" . 3) ("thu" . 3)
("friday" . 4) ("fri" . 4)
("saturday" . 5) ("sat" . 5)
("sunday" . 6) ("sun" . 6))
*weekday-strings*)
(hashlist '(("january" . 1) ("jan" . 1)
("february" . 2) ("feb" . 2)
("march" . 3) ("mar" . 3)
("april" . 4) ("apr" . 4)
("may" . 5) ("june" . 6)
("jun" . 6) ("july" . 7)
("jul" . 7) ("august" . 8)
("aug" . 8) ("september" . 9)
("sept" . 9) ("sep" . 9)
("october" . 10) ("oct" . 10)
("november" . 11) ("nov" . 11)
("december" . 12) ("dec" . 12))
*month-strings*)
(hashlist '(("gmt" . 0) ("est" . 5)
("edt" . 4) ("cst" . 6)
("cdt" . 5) ("mst" . 7)
("mdt" . 6) ("pst" . 8)
("pdt" . 7))
*zone-strings*)
(hashlist '(("yesterday" . yesterday) ("today" . today)
("tomorrow" . tomorrow) ("now" . now))
*special-strings*)
;;; Time/date format patterns are specified as lists of symbols repre-
;;; senting the elements. Optional elements can be specified by
;;; enclosing them in parentheses. Note that the order in which the
;;; patterns are specified below determines the order of search.
;;; Choices of pattern symbols are: second, minute, hour, day, month,
;;; year, time-divider, date-divider, am-pm, zone, weekday, noon-midn,
;;; and any special symbol.
(defparameter patterns
'(
;; Date formats.
((weekday) month (date-divider) day (date-divider) year (noon-midn))
((weekday) day (date-divider) month (date-divider) year (noon-midn))
((weekday) month (date-divider) day (noon-midn))
(year (date-divider) month (date-divider) day (noon-midn))
(month (date-divider) year (noon-midn))
(year (date-divider) month (noon-midn))
((noon-midn) (weekday) month (date-divider) day (date-divider) year)
((noon-midn) (weekday) day (date-divider) month (date-divider) year)
((noon-midn) (weekday) month (date-divider) day)
((noon-midn) year (date-divider) month (date-divider) day)
((noon-midn) month (date-divider) year)
((noon-midn) year (date-divider) month)
;; Time formats.
(hour (time-divider) (minute) (time-divider) (secondp) (am-pm)
(date-divider) (zone))
(noon-midn)
(hour (noon-midn))
;; Time/date combined formats.
((weekday) month (date-divider) day (date-divider) year
hour (time-divider) (minute) (time-divider) (secondp)
(am-pm) (date-divider) (zone))
((weekday) day (date-divider) month (date-divider) year
hour (time-divider) (minute) (time-divider) (secondp)
(am-pm) (date-divider) (zone))
((weekday) month (date-divider) day
hour (time-divider) (minute) (time-divider) (secondp)
(am-pm) (date-divider) (zone))
(year (date-divider) month (date-divider) day
hour (time-divider) (minute) (time-divider) (secondp)
(am-pm) (date-divider) (zone))
(month (date-divider) year
hour (time-divider) (minute) (time-divider) (secondp)
(am-pm) (date-divider) (zone))
(year (date-divider) month
hour (time-divider) (minute) (time-divider) (secondp)
(am-pm) (date-divider) (zone))
(hour (time-divider) (minute) (time-divider) (secondp) (am-pm)
(date-divider) (zone) (weekday) month (date-divider)
day (date-divider) year)
(hour (time-divider) (minute) (time-divider) (secondp) (am-pm)
(date-divider) (zone) (weekday) day (date-divider)
month (date-divider) year)
(hour (time-divider) (minute) (time-divider) (secondp) (am-pm)
(date-divider) (zone) (weekday) month (date-divider)
day)
(hour (time-divider) (minute) (time-divider) (secondp) (am-pm)
(date-divider) (zone) year (date-divider) month
(date-divider) day)
(hour (time-divider) (minute) (time-divider) (secondp) (am-pm)
(date-divider) (zone) month (date-divider) year)
(hour (time-divider) (minute) (time-divider) (secondp) (am-pm)
(date-divider) (zone) year (date-divider) month)
;; Weird, non-standard formats.
(weekday month day hour (time-divider) minute (time-divider)
secondp (am-pm)
(zone) year)
((weekday) day (date-divider) month (date-divider) year hour
(time-divider) minute (time-divider) (secondp) (am-pm)
(date-divider) (zone))
((weekday) month (date-divider) day (date-divider) year hour
(time-divider) minute (time-divider) (secondp) (am-pm)
(date-divider) (zone))
;; Special-string formats.
(now (yesterday))
((yesterday) now)
(now (today))
((today) now)
(now (tomorrow))
((tomorrow) now)
(yesterday (noon-midn))
((noon-midn) yesterday)
(today (noon-midn))
((noon-midn) today)
(tomorrow (noon-midn))
((noon-midn) tomorrow)
))
;;; The decoded-time structure holds the time/date values which are
;;; eventually passed to 'encode-universal-time' after parsing.
;;; Note: Currently nothing is done with the day of the week. It might
;;; be appropriate to add a function to see if it matches the date.
(defstruct decoded-time
(second 0 :type integer) ; Value between 0 and 59.
(minute 0 :type integer) ; Value between 0 and 59.
(hour 0 :type integer) ; Value between 0 and 23.
(day 1 :type integer) ; Value between 1 and 31.
(month 1 :type integer) ; Value between 1 and 12.
(year 1900 :type integer) ; Value above 1899 or between 0 and 99.
(zone 0 :type integer) ; Value between 0 and 23.
(dotw 0 :type integer)) ; Value between 0 and 6.
;;; Make-default-time returns a decoded-time structure with the default
;;; time values already set. The default time is currently 00:00 on
;;; the current day, current month, current year, and current time-zone.
(defun make-default-time (def-sec def-min def-hour def-day
def-mon def-year def-zone def-dotw)
(let ((default-time (make-decoded-time)))
(multiple-value-bind (sec min hour day mon year dotw dst zone)
(get-decoded-time)
(declare (ignore dst))
(if def-sec
(if (eq def-sec :current)
(setf (decoded-time-second default-time) sec)
(setf (decoded-time-second default-time) def-sec))
(setf (decoded-time-second default-time) 0))
(if def-min
(if (eq def-min :current)
(setf (decoded-time-minute default-time) min)
(setf (decoded-time-minute default-time) def-min))
(setf (decoded-time-minute default-time) 0))
(if def-hour
(if (eq def-hour :current)
(setf (decoded-time-hour default-time) hour)
(setf (decoded-time-hour default-time) def-hour))
(setf (decoded-time-hour default-time) 0))
(if def-day
(if (eq def-day :current)
(setf (decoded-time-day default-time) day)
(setf (decoded-time-day default-time) def-day))
(setf (decoded-time-day default-time) day))
(if def-mon
(if (eq def-mon :current)
(setf (decoded-time-month default-time) mon)
(setf (decoded-time-month default-time) def-mon))
(setf (decoded-time-month default-time) mon))
(if def-year
(if (eq def-year :current)
(setf (decoded-time-year default-time) year)
(setf (decoded-time-year default-time) def-year))
(setf (decoded-time-year default-time) year))
(if def-zone
(if (eq def-zone :current)
(setf (decoded-time-zone default-time) zone)
(setf (decoded-time-zone default-time) def-zone))
(setf (decoded-time-zone default-time) zone))
(if def-dotw
(if (eq def-dotw :current)
(setf (decoded-time-dotw default-time) dotw)
(setf (decoded-time-dotw default-time) def-dotw))
(setf (decoded-time-dotw default-time) dotw))
default-time)))
;;; Converts the values in the decoded-time structure to universal time
;;; by calling extensions:encode-universal-time.
;;; If zone is in numerical form, tweeks it appropriately.
(defun convert-to-unitime (parsed-values)
(let ((zone (decoded-time-zone parsed-values)))
(encode-universal-time (decoded-time-second parsed-values)
(decoded-time-minute parsed-values)
(decoded-time-hour parsed-values)
(decoded-time-day parsed-values)
(decoded-time-month parsed-values)
(decoded-time-year parsed-values)
(if (or (> zone 23) (< zone -23))
(let ((new-zone (/ zone 100)))
(cond ((minusp new-zone) (- new-zone))
((plusp new-zone) (- 24 new-zone))
;; must be zero (GMT)
(t new-zone)))
zone))))
;;; Sets the current values for the time and/or date parts of the
;;; decoded time structure.
(defun set-current-value (values-structure &key (time nil) (date nil) (zone nil))
(multiple-value-bind (sec min hour day mon year dotw dst tz)
(get-decoded-time)
(declare (ignore dst))
(when time
(setf (decoded-time-second values-structure) sec)
(setf (decoded-time-minute values-structure) min)
(setf (decoded-time-hour values-structure) hour))
(when date
(setf (decoded-time-day values-structure) day)
(setf (decoded-time-month values-structure) mon)
(setf (decoded-time-year values-structure) year)
(setf (decoded-time-dotw values-structure) dotw))
(when zone
(setf (decoded-time-zone values-structure) tz))))
;;; Special function definitions. To define a special substring, add
;;; a dotted pair consisting of the substring and a symbol in the
;;; *special-strings* hashlist statement above. Then define a function
;;; here which takes one argument- the decoded time structure- and
;;; sets the values of the structure to whatever is necessary. Also,
;;; add a some patterns to the patterns list using whatever combinations
;;; of special and pre-existing symbols desired.
(defun yesterday (parsed-values)
(set-current-value parsed-values :date t :zone t)
(setf (decoded-time-day parsed-values)
(1- (decoded-time-day parsed-values))))
(defun today (parsed-values)
(set-current-value parsed-values :date t :zone t))
(defun tomorrow (parsed-values)
(set-current-value parsed-values :date t :zone t)
(setf (decoded-time-day parsed-values)
(1+ (decoded-time-day parsed-values))))
(defun now (parsed-values)
(set-current-value parsed-values :time t))
;;; Predicates for symbols. Each symbol has a corresponding function
;;; defined here which is applied to a part of the datum to see if
;;; it matches the qualifications.
(defun am-pm (string)
(and (simple-string-p string)
(cond ((string= string "am") 'am)
((string= string "pm") 'pm)
(t nil))))
(defun noon-midn (string)
(and (simple-string-p string)
(cond ((string= string "noon") 'noon)
((string= string "midnight") 'midn)
(t nil))))
(defun weekday (string)
(and (simple-string-p string) (gethash string *weekday-strings*)))
(defun month (thing)
(or (and (simple-string-p thing) (gethash thing *month-strings*))
(and (integerp thing) (<= 1 thing 12))))
(defun zone (thing)
(or (and (simple-string-p thing) (gethash thing *zone-strings*))
(if (integerp thing)
(let ((zone (/ thing 100)))
(and (integerp zone) (<= -23 zone 23))))))
(defun special (string)
(and (simple-string-p string) (gethash string *special-strings*)))
(defun secondp (number)
(and (integerp number) (<= 0 number 59)))
(defun minute (number)
(and (integerp number) (<= 0 number 59)))
(defun hour (number)
(and (integerp number) (<= 0 number 23)))
(defun day (number)
(and (integerp number) (<= 1 number 31)))
(defun year (number)
(and (integerp number)
(or (<= 0 number 99)
(<= 1900 number))))
(defun time-divider (character)
(and (characterp character)
(member character time-dividers :test #'char=)))
(defun date-divider (character)
(and (characterp character)
(member character date-dividers :test #'char=)))
;;; Match-substring takes a string argument and tries to match it with
;;; the strings in one of the four hash tables: *weekday-strings*, *month-
;;; strings*, *zone-strings*, *special-strings*. It returns a specific
;;; keyword and/or the object it finds in the hash table. If no match
;;; is made then it immediately signals an error.
(defun match-substring (substring)
(let ((substring (nstring-downcase substring)))
(or (let ((test-value (month substring)))
(if test-value (cons 'month test-value)))
(let ((test-value (weekday substring)))
(if test-value (cons 'weekday test-value)))
(let ((test-value (am-pm substring)))
(if test-value (cons 'am-pm test-value)))
(let ((test-value (noon-midn substring)))
(if test-value (cons 'noon-midn test-value)))
(let ((test-value (zone substring)))
(if test-value (cons 'zone test-value)))
(let ((test-value (special substring)))
(if test-value (cons 'special test-value)))
(if *error-on-mismatch*
(error "\"~A\" is not a recognized word or abbreviation."
substring)
(return-from match-substring nil)))))
;;; Decompose-string takes the time/date string and decomposes it into a
;;; list of alphabetic substrings, numbers, and special divider characters.
;;; It matches whatever strings it can and replaces them with a dotted pair
;;; containing a symbol and value.
(defun decompose-string (string &key (start 0) (end (length string)) (radix 10))
(do ((string-index start)
(next-negative nil)
(parts-list nil))
((eq string-index end) (nreverse parts-list))
(let ((next-char (char string string-index))
(prev-char (if (= string-index start)
nil
(char string (1- string-index)))))
(cond ((alpha-char-p next-char)
;; Alphabetic character - scan to the end of the substring.
(do ((scan-index (1+ string-index) (1+ scan-index)))
((or (eq scan-index end)
(not (alpha-char-p (char string scan-index))))
(let ((match-symbol (match-substring
(subseq string string-index scan-index))))
(if match-symbol
(push match-symbol parts-list)
(return-from decompose-string nil)))
(setf string-index scan-index))))
((digit-char-p next-char radix)
;; Numeric digit - convert digit-string to a decimal value.
(do ((scan-index string-index (1+ scan-index))
(numeric-value 0 (+ (* numeric-value radix)
(digit-char-p (char string scan-index) radix))))
((or (eq scan-index end)
(not (digit-char-p (char string scan-index) radix)))
;; If next-negative is t, set the numeric value to it's
;; opposite and reset next-negative to nil.
(when next-negative
(setf next-negative nil)
(setf numeric-value (- numeric-value)))
(push numeric-value parts-list)
(setf string-index scan-index))))
((and (char= next-char #\-)
(or (not prev-char)
(member prev-char whitespace-chars :test #'char=)))
;; If we see a minus sign before a number, but not after one,
;; it is not a date divider, but a negative offset from GMT, so
;; set next-negative to t and continue.
(setf next-negative t)
(incf string-index))
((member next-char time-dividers :test #'char=)
;; Time-divider - add it to the parts-list with symbol.
(push (cons 'time-divider next-char) parts-list)
(incf string-index))
((member next-char date-dividers :test #'char=)
;; Date-divider - add it to the parts-list with symbol.
(push (cons 'date-divider next-char) parts-list)
(incf string-index))
((member next-char whitespace-chars :test #'char=)
;; Whitespace character - ignore it completely.
(incf string-index))
((char= next-char #\()
;; Parenthesized string - scan to the end and ignore it.
(do ((scan-index string-index (1+ scan-index)))
((or (eq scan-index end)
(char= (char string scan-index) #\)))
(setf string-index (1+ scan-index)))))
(t
;; Unrecognized character - barf voraciously.
(if *error-on-mismatch*
(error (concatenate 'simple-string ">>> " string
"~%~VT^-- Bogus character encountered here.")
(+ string-index 4))
(return-from decompose-string nil)))))))
;;; Match-pattern-element tries to match a pattern element with a datum
;;; element and returns the symbol associated with the datum element if
;;; successful. Otherwise nil is returned.
(defun match-pattern-element (pattern-element datum-element)
(cond ((listp datum-element)
(let ((datum-type (if (eq (car datum-element) 'special)
(cdr datum-element)
(car datum-element))))
(if (eq datum-type pattern-element) datum-element)))
((funcall pattern-element datum-element)
(cons pattern-element datum-element))
(t nil)))
;;; Match-pattern matches a pattern against a datum, returning the
;;; pattern if successful and nil otherwise.
(defun match-pattern (pattern datum datum-length)
(if (>= (length pattern) datum-length)
(let ((form-list nil))
(do ((pattern pattern (cdr pattern))
(datum datum (cdr datum)))
((or (null pattern) (null datum))
(cond ((and (null pattern) (null datum))
(nreverse form-list))
((null pattern) nil)
((null datum) (dolist (element pattern
(nreverse form-list))
(if (not (listp element))
(return nil))))))
(let* ((pattern-element (car pattern))
(datum-element (car datum))
(optional (listp pattern-element))
(matching (match-pattern-element (if optional
(car pattern-element)
pattern-element)
datum-element)))
(cond (matching (let ((form-type (car matching)))
(unless (or (eq form-type 'time-divider)
(eq form-type 'date-divider))
(push matching form-list))))
(optional (push datum-element datum))
(t (return-from match-pattern nil))))))))
;;; Deal-with-noon-midn sets the decoded-time values to either noon
;;; or midnight depending on the argument form-value. Form-value
;;; can be either 'noon or 'midn.
(defun deal-with-noon-midn (form-value parsed-values)
(cond ((eq form-value 'noon)
(setf (decoded-time-hour parsed-values) 12))
((eq form-value 'midn)
(setf (decoded-time-hour parsed-values) 0))
(t (error "Unrecognized symbol: ~A" form-value)))
(setf (decoded-time-minute parsed-values) 0)
(setf (decoded-time-second parsed-values) 0))
;;; Deal-with-am-pm sets the decoded-time values to be in the am
;;; or pm depending on the argument form-value. Form-value can
;;; be either 'am or 'pm.
(defun deal-with-am-pm (form-value parsed-values)
(let ((hour (decoded-time-hour parsed-values)))
(cond ((eq form-value 'am)
(cond ((eq hour 12)
(setf (decoded-time-hour parsed-values) 0))
((not (<= 0 hour 12))
(if *error-on-mismatch*
(error "~D is not an AM hour, dummy." hour)))))
((eq form-value 'pm)
(if (<= 0 hour 11)
(setf (decoded-time-hour parsed-values)
(mod (+ hour 12) 24))))
(t (error "~A isn't AM/PM - this shouldn't happen.")))))
;;; Set-time-values uses the association list of symbols and values
;;; to set the time in the decoded-time structure.
(defun set-time-values (string-form parsed-values)
(dolist (form-part string-form t)
(let ((form-type (car form-part))
(form-value (cdr form-part)))
(case form-type
(secondp (setf (decoded-time-second parsed-values) form-value))
(minute (setf (decoded-time-minute parsed-values) form-value))
(hour (setf (decoded-time-hour parsed-values) form-value))
(day (setf (decoded-time-day parsed-values) form-value))
(month (setf (decoded-time-month parsed-values) form-value))
(year (setf (decoded-time-year parsed-values) form-value))
(zone (setf (decoded-time-zone parsed-values) form-value))
(weekday (setf (decoded-time-dotw parsed-values) form-value))
(am-pm (deal-with-am-pm form-value parsed-values))
(noon-midn (deal-with-noon-midn form-value parsed-values))
(special (funcall form-value parsed-values))
(t (error "Unrecognized symbol in form list: ~A." form-type))))))
(defun parse-time (time-string &key (start 0) (end (length time-string))
(error-on-mismatch nil)
(default-seconds nil) (default-minutes nil)
(default-hours nil) (default-day nil)
(default-month nil) (default-year nil)
(default-zone nil) (default-weekday nil))
"Tries very hard to make sense out of the argument time-string and
returns a single integer representing the universal time if
successful. If not, it returns nil. If the :error-on-mismatch
keyword is true, parse-time will signal an error instead of
returning nil. Default values for each part of the time/date
can be specified by the appropriate :default- keyword. These
keywords can be given a numeric value or the keyword :current
to set them to the current value. The default-default values
are 00:00:00 on the current date, current time-zone."
(setq *error-on-mismatch* error-on-mismatch)
(let* ((string-parts (decompose-string time-string :start start :end end))
(parts-length (length string-parts))
(string-form (dolist (pattern patterns)
(let ((match-result (match-pattern pattern
string-parts
parts-length)))
(if match-result (return match-result))))))
(if string-form
(let ((parsed-values (make-default-time default-seconds default-minutes
default-hours default-day
default-month default-year
default-zone default-weekday)))
(set-time-values string-form parsed-values)
(convert-to-unitime parsed-values))
(if *error-on-mismatch*
(error "\"~A\" is not a recognized time/date format." time-string)
nil))))